# AggregateAccessPointToColumnMapping.java

    package org.codefilarete.stalactite.engine.runtime.query;

    import java.util.ArrayDeque;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Deque;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Queue;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    import org.codefilarete.reflection.AccessorByMethodReference;
    import org.codefilarete.reflection.AccessorDefinition;
    import org.codefilarete.reflection.PropertyAccessPoint;
    import org.codefilarete.reflection.PropertyMutator;
    import org.codefilarete.reflection.ReadWriteAccessPoint;
    import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
    import org.codefilarete.reflection.ValueAccessPoint;
    import org.codefilarete.reflection.ValueAccessPointChain;
    import org.codefilarete.stalactite.dsl.MappingConfigurationException;
    import org.codefilarete.stalactite.engine.configurer.DefaultComposedIdentifierAssembler;
    import org.codefilarete.stalactite.engine.configurer.builder.BuildLifeCycleListener;
    import org.codefilarete.stalactite.engine.configurer.builder.PersisterBuilderContext;
    import org.codefilarete.stalactite.engine.configurer.elementcollection.ElementRecordMapping;
    import org.codefilarete.stalactite.engine.configurer.map.KeyValueRecordMapping;
    import org.codefilarete.stalactite.engine.runtime.load.AbstractJoinNode;
    import org.codefilarete.stalactite.engine.runtime.load.EntityInflater;
    import org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree;
    import org.codefilarete.stalactite.engine.runtime.load.JoinNode;
    import org.codefilarete.stalactite.engine.runtime.load.JoinRoot;
    import org.codefilarete.stalactite.engine.runtime.load.RelationJoinNode;
    import org.codefilarete.stalactite.engine.runtime.load.TablePerClassRootJoinNode;
    import org.codefilarete.stalactite.engine.runtime.query.EntityCriteriaSupport.AccessorToColumnMap;
    import org.codefilarete.stalactite.mapping.AccessorWrapperIdAccessor;
    import org.codefilarete.stalactite.mapping.EntityMapping;
    import org.codefilarete.stalactite.mapping.id.assembly.IdentifierAssembler;
    import org.codefilarete.stalactite.mapping.id.assembly.SingleIdentifierAssembler;
    import org.codefilarete.stalactite.query.api.JoinLink;
    import org.codefilarete.stalactite.query.api.QueryStatement.PseudoTable;
    import org.codefilarete.stalactite.query.api.Selectable;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.StringAppender;
    import org.codefilarete.tool.VisibleForTesting;
    import org.codefilarete.tool.collection.Arrays;

    import static org.codefilarete.stalactite.engine.configurer.elementcollection.ElementRecord.ELEMENT_ACCESSOR;
    import static org.codefilarete.stalactite.engine.configurer.map.KeyValueRecord.VALUE_ACCESSOR;

    /**
     * Maps the aggregate property accessors points to their column: relations are taken into account, that's the main benefit of it.
     * Thus, anyone can get the column behind an accessor chain.
     * The mapping is collected through the {@link EntityJoinTree} by looking for {@link RelationJoinNode} and their {@link EntityMapping}
     * (actually the algorithm is more complex).
     */
    public class AggregateAccessPointToColumnMapping<C> {

    	/**
    	 * Owned properties mapping
    	 * The implementation is based on {@link AccessorDefinition} comparison as keys. This can be a quite heavy comparison but we have no other
    	 * choice : propertyToColumn is built from entity mapping which can contains
    	 * - {@link ReadWriteAccessPoint} (for exemple) whereas getColumn() will get
    	 * - {@link AccessorByMethodReference} which are quite different but should be compared.
    	 */
    	private final Map<List<? extends ValueAccessPoint<?>>, JoinLink<?, ?>> propertyToColumn = new AccessorToColumnMap();

    	private final EntityJoinTree<C, ?> tree;

    	@VisibleForTesting
    	AggregateAccessPointToColumnMapping(EntityJoinTree<C, ?> tree, boolean withImmediatePropertiesCollect) {
    		this.tree = tree;

    		if (withImmediatePropertiesCollect) {
    			collectPropertiesMapping();
    		} else {
    			PersisterBuilderContext.CURRENT.get().addBuildLifeCycleListener(new BuildLifeCycleListener() {
    				@Override
    				public void afterBuild() {
    					collectPropertiesMapping();
    				}

    				@Override
    				public void afterAllBuild() {

    				}
    			});
    		}
    	}

    	public boolean hasCollectionProperty() {
    		return propertyToColumn.keySet().stream().flatMap(Collection::stream)
    				.anyMatch(valueAccessPoint -> Iterable.class.isAssignableFrom(AccessorDefinition.giveDefinition(valueAccessPoint).getMemberType()));
    	}

    	@VisibleForTesting
    	public Map<List<? extends ValueAccessPoint<?>>, JoinLink<?, ?>> getPropertyToColumn() {
    		return propertyToColumn;
    	}

    	private void collectPropertiesMapping() {
    		Deque<RelationJoinNode<?, ?, ?, ?, ?>> accessorPath = new ArrayDeque<>();
    		Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> rootProperties = collectPropertiesMapping(tree.getRoot(), new ArrayList<>());
    		rootProperties.forEach((valueAccessPoints, selectable) -> {
    			this.propertyToColumn.put(valueAccessPoints, tree.getRoot().getOriginalColumnsToLocalOnes().get(selectable));
    		});
    		Queue<AbstractJoinNode<?, ?, ?, ?>> stack = Collections.asLifoQueue(new ArrayDeque<>());
    		stack.addAll(tree.getRoot().getJoins());
    		while (!stack.isEmpty()) {
    			AbstractJoinNode<?, ?, ?, ?> abstractJoinNode = stack.poll();
    			if (abstractJoinNode instanceof RelationJoinNode) {
    				RelationJoinNode<?, ?, ?, ?, ?> relationJoinNode = (RelationJoinNode<?, ?, ?, ?, ?>) abstractJoinNode;
    				accessorPath.add(relationJoinNode);
    				Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> joinNodeProperties = collectPropertiesMapping(relationJoinNode, accessorPath.stream().map(RelationJoinNode::getPropertyAccessor).collect(Collectors.toSet()));
    				joinNodeProperties.forEach((valueAccessPoints, selectable) -> {
    					this.propertyToColumn.put(valueAccessPoints, relationJoinNode.getOriginalColumnsToLocalOnes().get(selectable));
    				});
    				if (abstractJoinNode.getJoins().isEmpty()) {
    					// no more joins, this is a leaf, we remove the whole branch
    					do {
    						accessorPath.removeLast();
    					} while ((accessorPath.peekLast() != null ? accessorPath.peekLast().getJoins().size() : 0) == 1);
    				}
    			}
    			stack.addAll(abstractJoinNode.getJoins());
    		}
    	}

    	/**
    	 * Collect properties, read-only properties, and identifier column mapping from given {@link EntityMapping}
    	 *
    	 * @param joinNode the node from which we can get a {@link EntityMapping} to collect all properties from
    	 */
    	private <E> Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> collectPropertiesMapping(JoinNode<E, ?> joinNode, Collection<PropertyAccessPoint<?, ?>> accessorPath) {
    		EntityInflater<E, ?> entityInflater;
    		if (joinNode instanceof JoinRoot) {
    			if (joinNode instanceof TablePerClassRootJoinNode) {
    				entityInflater = ((TablePerClassRootJoinNode<E, ?>) joinNode).getEntityInflater();
    				EntityMapping<E, ?, ?> entityMapping = entityInflater.getEntityMapping();
    				PseudoTable pseudoTable = ((TablePerClassRootJoinNode<E, ?>) joinNode).getTable();
    				// Because we have a Union (behind the pseudo table), there's no mapping with "original ones" (we are in table-per-class)
    				// so we provide a small column "adapter" that lookup for the columns in the union
    				return collectPropertiesMapping(entityMapping, accessorPath, col -> pseudoTable.findColumn(col.getExpression()));
    			} else {
    				// non-polymorphic root, aka simple case
    				entityInflater = ((JoinRoot<E, ?, ?>) joinNode).getEntityInflater();
    			}
    		} else if (joinNode instanceof RelationJoinNode) {
    			entityInflater = ((RelationJoinNode<E, ?, ?, ?, ?>) joinNode).getEntityInflater();

    			// Note about complex type handling: The goal of all this code is to suit the need of Spring Data and its derived queries. There are few
    			// reasons (for now) to adhere to any other framework or API (meanwhile we are open to do it, but for now, that's the status of the
    			// project), thus, because Spring-Data doesn't support complex type (in particular for Maps), we don't also.
    			// see Spring discussion about Collection and Map here https://github.com/spring-projects/spring-data-commons/issues/2504
    			EntityMapping<E, ?, ?> entityMapping = entityInflater.getEntityMapping();
    			if (entityMapping instanceof ElementRecordMapping) {    // Collection mapping case
    				// Querying Collection is only possible on its values (obviously !)
    				Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> result = new HashMap<>();
    				Column<?, ?> mapValueColumn = entityMapping.getPropertyToColumn().get(ELEMENT_ACCESSOR);
    				List<PropertyAccessPoint<?, ?>> accessorPrefix = new ArrayList<>(accessorPath);
    				result.put(accessorPrefix, mapValueColumn);
    				return result;
    			} else if (entityMapping instanceof KeyValueRecordMapping) {    // Map mapping case
    				// Querying Map is only possible on its values
    				Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> result = new HashMap<>();
    				Column<?, ?> mapValueColumn = entityMapping.getPropertyToColumn().get(VALUE_ACCESSOR);
    				List<PropertyAccessPoint<?, ?>> accessorPrefix = new ArrayList<>(accessorPath);
    				result.put(accessorPrefix, mapValueColumn);
    				return result;
    			}
    		} else {
    			// this should not happen because we master the node types that support getEntityInflater !
    			throw new UnsupportedOperationException("Unsupported join type " + Reflections.toString(joinNode.getClass()));
    		}
    		EntityMapping<E, ?, ?> entityMapping = entityInflater.getEntityMapping();
    		return collectPropertiesMapping(entityMapping, accessorPath, Function.identity());
    	}

    	private <E> Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> collectPropertiesMapping(EntityMapping<E, ?, ?> entityMapping,
    																							 Collection<PropertyAccessPoint<?, ?>> nodeAccessorPath,
    																							 Function<Selectable<?>, Selectable<?>> columnAdapter) {
    		Map<List<PropertyAccessPoint<?, ?>>, Selectable<?>> result = new HashMap<>();

    		// Collecting basic direct properties
    		Stream.concat(entityMapping.getPropertyToColumn().entrySet().stream(), entityMapping.getReadonlyPropertyToColumn().entrySet().stream())
    				.forEach((entry) -> {
    					PropertyMutator<E, ?> accessor = entry.getKey();
    					List<PropertyAccessPoint<?, ?>> key;
    					if (accessor instanceof ValueAccessPointChain) {
    						// - we can expect ValueAccessPointChain accessors to be of PropertyAccessPoint type (because the ORM
    						// doesn't support persistence for indexed value like ListAccessor or ArrayAccessor)
    						// - we make a copy of the List because it will be modified some line below to add the accessors prefix
    						key = new ArrayList<>((List<? extends PropertyAccessPoint<?, ?>>) ((ValueAccessPointChain) accessor).getAccessors());
    					} else {
    						key = Arrays.asList(accessor);
    					}
    					result.put(key, columnAdapter.apply(entry.getValue()));
    				});

    		// collecting the identifier mapping (because they are not in the properties mapping)
    		IdentifierAssembler<?, ?> identifierAssembler = entityMapping.getIdMapping().getIdentifierAssembler();
    		if (identifierAssembler instanceof SingleIdentifierAssembler) {
    			Column idColumn = ((SingleIdentifierAssembler) identifierAssembler).getColumn();
    			ReadWritePropertyAccessPoint idAccessor = ((AccessorWrapperIdAccessor) entityMapping.getIdMapping().getIdAccessor()).getIdAccessor();
    			result.put(Arrays.asList(idAccessor), columnAdapter.apply(idColumn));
    		} else if (identifierAssembler instanceof DefaultComposedIdentifierAssembler) {
    			((DefaultComposedIdentifierAssembler<?, ?>) identifierAssembler).getMapping().forEach((idAccessor, idColumn) -> {
    				ReadWritePropertyAccessPoint accessorPrefix = ((AccessorWrapperIdAccessor) entityMapping.getIdMapping().getIdAccessor()).getIdAccessor();
    				result.put(Arrays.asList(accessorPrefix, idAccessor), columnAdapter.apply(idColumn));
    			});
    		}

    		// adding the readWriteAccessPoint prefix to all the found properties, to create an readWriteAccessPoint available from the aggregate root
    		result.forEach((k, v) -> {
    			k.addAll(0, nodeAccessorPath);
    		});
    		return result;
    	}

    	/**
    	 * Gives the column of an access points {@link List}.
    	 * Most of the time result is a 1-size collection, but in the case of complex type (as for a composite key), the result is a collection of columns.
    	 * Note that it supports "many" accessor: given parameter acts as a "transport" of accessors, we don't use
    	 * its functionality such as get() or toMutator() hence it's not necessary that it be consistent. For example,
    	 * it can start with a Collection accessor then an accessor to the component of the Collection. See
    	 *
    	 * @param valueAccessPoints chain of accessors to a property that has a matching column
    	 * @return the found column, throws an exception if not found
    	 */
    	public JoinLink<?, ?> giveColumn(List<? extends ValueAccessPoint<?>> valueAccessPoints) {
    		// looking among current properties
    		JoinLink<?, ?> column = this.propertyToColumn.get(valueAccessPoints);
    		if (column != null) {
    			return column;
    		} else {
    			throw newConfigurationException(valueAccessPoints);
    		}
    	}

    	private MappingConfigurationException newConfigurationException(List<? extends ValueAccessPoint<?>> valueAccessPoints) {
    		StringAppender accessPointAsString = new StringAppender() {
    			@Override
    			public StringAppender cat(Object o) {
    				if (o instanceof ValueAccessPoint) {
    					super.cat(AccessorDefinition.toString((ValueAccessPoint) o));
    				} else {
    					super.cat(o);
    				}
    				return this;
    			}
    		};
    		accessPointAsString.ccat(valueAccessPoints, " > ");
    		return new MappingConfigurationException("Error while looking for column of " + accessPointAsString
    				+ " : it is not declared in mapping of " + Reflections.toString(this.tree.getRoot().getEntityInflater().getEntityType()));
    	}
    }